package com.rub.android.utils.parser

import android.util.Xml
import androidx.annotation.NonNull

import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import org.xmlpull.v1.XmlSerializer

import java.io.IOException
import java.io.InputStream
import java.io.StringReader
import java.io.StringWriter
import java.lang.Exception
import java.lang.RuntimeException
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.HashSet
import java.util.Locale

import javax.xml.transform.OutputKeys
import javax.xml.transform.Source
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource

/**
 * Converts JSON to XML
 */
class JsonToXml private constructor(
    @param:NonNull private val mJson: JSONObject?,
    @param:NonNull private val mForcedAttributes: HashSet<String>,
    private val mForcedContent: HashSet<String>
) {
    class Builder {
        private var mJson: JSONObject? = null
        private val mForcedAttributes = HashSet<String>()
        private val mForcedContent = HashSet<String>()

        /**
         * Constructor
         * @param jsonObject a JSON object
         */
        constructor(@NonNull jsonObject: JSONObject?) {
            mJson = jsonObject
        }

        /**
         * Constructor
         * @param inputStream InputStream containing the JSON
         */
        constructor(@NonNull inputStream: InputStream?) : this(
            FileReader.readFileFromInputStream(
                inputStream
            )
        ) {
        }

        /**
         * Constructor
         * @param jsonString String containing the JSON
         */
        constructor(jsonString: String?) {
            try {
                mJson = JSONObject(jsonString)
            } catch (exception: JSONException) {
                exception.printStackTrace()
            }
        }

        /**
         * Force a TAG to be an attribute of the parent TAG
         * @param path Path for the attribute, using format like "/parentTag/childTag/childTagAttribute"
         * @return the Builder
         */
        fun forceAttribute(path: String): Builder {
            mForcedAttributes.add(path)
            return this
        }

        /**
         * Force a TAG to be the content of its parent TAG
         * @param path Path for the content, using format like "/parentTag/contentTag"
         * @return the Builder
         */
        fun forceContent(path: String): Builder {
            mForcedContent.add(path)
            return this
        }

        /**
         * Creates the JsonToXml object
         * @return a JsonToXml instance
         */
        fun build(): JsonToXml {
            return JsonToXml(mJson, mForcedAttributes, mForcedContent)
        }
    }

    /**
     *
     * @return the XML
     */
    override fun toString(): String {
        val rootNode = Node(null, "")
        prepareObject(rootNode, mJson)
        return nodeToXML(rootNode)
    }
    /**
     *
     * @param indent size of the indent (number of spaces)
     * @return the formatted XML
     */
    /**
     *
     * @return the formatted XML with a default indent (3 spaces)
     */
    @JvmOverloads
    fun toFormattedString(indent: Int = DEFAULT_INDENTATION): String {
        val input = toString()
        return try {
            val xmlInput: Source = StreamSource(StringReader(input))
            val stringWriter = StringWriter()
            val xmlOutput = StreamResult(stringWriter)
            val transformerFactory = TransformerFactory.newInstance()
            val transformer = transformerFactory.newTransformer()
            transformer.setOutputProperty(OutputKeys.INDENT, "yes")
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "" + indent)
            transformer.transform(xmlInput, xmlOutput)
            xmlOutput.writer.toString()
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }

    private fun nodeToXML(node: Node): String {
        val serializer = Xml.newSerializer()
        val writer = StringWriter()
        return try {
            serializer.setOutput(writer)
            serializer.startDocument("UTF-8", true)
            nodeToXml(serializer, node)
            serializer.endDocument()
            writer.toString()
        } catch (e: IOException) {
            throw RuntimeException(e)
        }
    }

    @Throws(IOException::class)
    private fun nodeToXml(serializer: XmlSerializer, node: Node) {
        val nodeName: String? = node.name
        if (nodeName != null) {
            serializer.startTag("", nodeName)
            for (attribute in node.attributes) {
                serializer.attribute("", attribute.mKey, attribute.mValue)
            }
            val nodeContent: String? = node.content
            if (nodeContent != null) {
                serializer.text(nodeContent)
            }
        }
        for (subNode in node.children) {
            nodeToXml(serializer, subNode)
        }
        if (nodeName != null) {
            serializer.endTag("", nodeName)
        }
    }

    private fun prepareObject(node: Node, json: JSONObject?) {
        val keyterator = json!!.keys()
        while (keyterator.hasNext()) {
            val key = keyterator.next()
            val `object` = json.opt(key)
            if (`object` != null) {
                if (`object` is JSONObject) {
                    val path: String = node.path.toString() + "/" + key
                    val subNode = Node(key, path)
                    node.addChild(subNode)
                    prepareObject(subNode, `object`)
                } else if (`object` is JSONArray) {
                    prepareArray(node, key, `object`)
                } else {
                    val path: String = node.path.toString() + "/" + key
                    // JSON numbers are represented either Integer or Double (IEEE 754)
                    // Long may be represented in scientific notation because they are stored as Double
                    // This workaround attempts to represent Long and Double objects accordingly
                    var value: String?
                    if (`object` is Double) {
                        val d = `object`
                        // If it is a Long
                        if (d % 1 == 0.0) {
                            value = java.lang.Long.toString(d.toLong())
                        } else {
                            // TODO: Set up number of decimal digits per attribute in the builder
                            // Set only once. Represent all double numbers up to 20 decimal digits
                            if (DECIMAL_FORMAT.maximumFractionDigits == 0) {
                                DECIMAL_FORMAT.maximumFractionDigits = 20
                            }
                            value = DECIMAL_FORMAT.format(d)
                        }
                    } else {
                        // Integer, Boolean and String are handled here
                        value = `object`.toString()
                    }
                    if (isAttribute(path)) {
                        node.addAttribute(key, value)
                    } else if (isContent(path)) {
                        node.content=value
                    } else {
                        val subNode = Node(key, node.path)
                        subNode.content=value
                        node.addChild(subNode)
                    }
                }
            }
        }
    }

    private fun prepareArray(node: Node, key: String, array: JSONArray) {
        val count = array.length()
        val path: String = node.path.toString() + "/" + key
        for (i in 0 until count) {
            val subNode = Node(key, path)
            val `object` = array.opt(i)
            if (`object` != null) {
                if (`object` is JSONObject) {
                    prepareObject(subNode, `object`)
                } else if (`object` is JSONArray) {
                    prepareArray(subNode, key, `object`)
                } else {
                    val value = `object`.toString()
                    subNode.name=key
                    subNode.content=value
                }
            }
            node.addChild(subNode)
        }
    }

    private fun isAttribute(path: String): Boolean {
        return mForcedAttributes.contains(path)
    }

    private fun isContent(path: String): Boolean {
        return mForcedContent.contains(path)
    }

    companion object {
        private const val DEFAULT_INDENTATION = 3

        private val DECIMAL_FORMAT = DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH))
    }
}
